package org.xmlbinder;

import java.lang.reflect.*;
import java.util.*;

/** The class that processes annotations in a field and caches the data for the Injector
 * @author Noble Paul (noble.paul@gmail.com)
 *         Date: Nov 20, 2007
 *         Time: 11:38:57 AM
 */
public class FieldAnnotationProcessor extends FieldInjector{

    public FieldAnnotationProcessor(LinkedList<String> names, FieldAnnotationProcessor aField, BeanAnnotationProcessor parent) {
        this.parent = parent;
        name = names.removeFirst();
        if (names.size() > 0) {
            isElem = isEmpty = true;
            findOrCreateBean();
            bean.buildMeta(names, aField,this);
        } else {
            copyFrom(aField);
            if(aField.isEndTag || aField.isStartTag){
                aField.isEndTag = aField.isStartTag = false;
                //do not proceed further. return
            } else if (aField.isElem) {
                isElem = true;
                bean = new BeanAnnotationProcessor(aField);
                bean.buildMeta(this);
            } else {
                isAttr = aField.isElem = true;//this field becomes an attribute and the original field is set to an element
                aField.isAttr = false;
            }
        }
    }

    private void copyFrom(FieldAnnotationProcessor aField) {
        type = aField.type;
        field = aField.field;
        getter = aField.getter;
        setter = aField.setter;
        isArray = aField.isArray;
        isList = aField.isList;
        isCollection = aField.isCollection;
        isSet = aField.isSet;
        isStartTag =  aField.isStartTag;
        isEndTag =  aField.isEndTag;
    }

    void init(Field fld) {
        type = fld.getType();
        setFieldType();
        field = fld;
    }

    private void setFieldType() {
        if (type.isArray()) {
            isArray = true;
            type = type.getComponentType();
        } else if (type == Collection.class) {
            isCollection = true;
        } else if (type == List.class) {
            isList = true;
            isCollection = true;
        } else if (type == Set.class) {
            isCollection = true;
            isSet = true;
        } else if (type == Properties.class) {
            isProp = true;
        } else if (type == Map.class) {
            isMap = true;
        }
    }

    void init(Method meth) {
        Class[] params = meth.getParameterTypes();
        if(params.length ==0){
            if(meth.getName().startsWith("startTag")){
                isStartTag = true;
                setter = meth;
                return;
            } else if(meth.getName().startsWith("endTag")){
                isEndTag =  true;
                setter = meth;
                return;
            }
        }
        if (params.length != 1)
            throw new RuntimeException("Invalid setter method. Must have one and only one parameter");
        type = params[0];
        setFieldType();
        setter = meth;
        String setterName = setter.getName();
        if (setterName.startsWith("set")) {
            String getterName = setterName.replaceFirst("set", "get");
            try {
                getter = meth.getDeclaringClass().getMethod(getterName);
            } catch (Exception e) {
            }
        }
    }

    FieldAnnotationProcessor(String n, BeanAnnotationProcessor parent, AccessibleObject m) {
        name = n;
        this.parent = parent;
        m.setAccessible(true);
        if (m instanceof Field) {
            init((Field) m);
        } else {
            init((Method) m);
        }
        if (m.isAnnotationPresent(Attribute.class)) {
            isAttr = true;
            checkAttrTypeAndName(m);
            handleSlash();
        } else if (m.isAnnotationPresent(Cdata.class) ) {
            isCdata = true;
        } else if (m.isAnnotationPresent(Element.class)) {
            isElem = true;
            checkElemTypeAndName(m);
            if (!handleSlash()) {// if slash does not exist
                checkWildCard(Collections.<String>emptyList());
                if(isEndTag || isStartTag) return;
                bean = new BeanAnnotationProcessor(this);
                bean.buildMeta(this);
            }
        }
    }

    private void checkAttrTypeAndName(AccessibleObject m) {
        if(isEndTag || isStartTag) throw new RuntimeException("Attributes cannot be 'startTag' or 'endTag'");

        Attribute attr = m.getAnnotation(Attribute.class);
        String xAttrName=null;

        String attrName = attr == null ? xAttrName : attr.value();
        Class theType = attr == null ? String.class : attr.type();
        if (name == null) name = attrName;
        handleDefaultName();
        if ("*".equals(name)) parent.attrWildCard = this;
        if (isCollection || isProp || isMap) {
            type = theType;
            readTypeFromGeneric();
        }
        if (!BeanAnnotationProcessor.isSimple(type)) throw new RuntimeException("Attribute cannot be a complex type");
    }

    /**Special handling for skip level mapping
     *
     * @return
     */
    private boolean handleSlash() {
        String[] names = name.split("/");
        if (names.length == 1) return false;
        isEmpty = true;
        LinkedList<String> elems = new LinkedList<String>(Arrays.asList(names));
        name = elems.removeFirst();
        boolean wildWildCard = checkWildCard(elems);
        findOrCreateBean();
        bean.hasWildWildCard = wildWildCard;
        bean.buildMeta(elems, this,this);
        return true;
    }

    /** Special handling for wildcard mapping
     * @param elems
     * @return
     */
    private boolean checkWildCard(List<String> elems) {
        if ("*".equals(name)) parent.elemWildcard = this;
        if ("**".equals(name)) {
            if (elems.size() == 0) throw new RuntimeException("A ** must have an element name after '/'");
            String elem = null;
            for (Iterator<String> it = elems.iterator(); it.hasNext(); elem = it.next()) {
                if ("**".equals(elem) || "*".equals(elem)) {
                    it.remove();
                }
            }
            parent.elemWildcard = this;
            name = "*";
            return true;

        }
        return false;
    }

    private void findOrCreateBean() {
        FieldAnnotationProcessor duplicateFied = parent.elemFields.get(name);
        bean = duplicateFied == null ? new BeanAnnotationProcessor() : duplicateFied.bean;
    }

    /**If name is not given, use default name as if it is a java bean
     */
    private void handleDefaultName() {
        if (!"##default".equals(name)){
            return;
        } else if(isStartTag || isEndTag ){
             throw new RuntimeException("Must have the 'value' set for startTag and endTag");
        }
        if (field != null) {
            name = field.getName();
        } else if (setter != null) {
            String setterName = setter.getName();
            if (setterName.startsWith("set") && setterName.length() > 3) {
                name = setterName.substring(3, 4).toUpperCase() + setterName.substring(4);
            }
        }
    }

    /**Read the type of the field and the name of the field
     * @param m
     */
    private void checkElemTypeAndName(AccessibleObject m ) {
        Element e = m.getAnnotation(Element.class);
        String xename = null;
        Class xeType = null;

        String elemname = e == null ? xename : e.value();
//        Class theType = e == null ? xeType : e.type();
        if (name == null) name = elemname;
        handleDefaultName();
        if(isStartTag || isEndTag) return;
        if (isCollection || isProp || isMap) {
            type = e == null ? xeType : e.type();
            readTypeFromGeneric();
        }

    }

    /**For fields which are of collection types read the type info from the generics
     */
    private void readTypeFromGeneric() {
        if (field != null) {
            if (!(field.getGenericType() instanceof ParameterizedType)) return;
            ParameterizedType type = (ParameterizedType) field.getGenericType();
            Type[] types = type.getActualTypeArguments();
            if (isCollection && types != null && types.length > 0) {
                this.type = (Class) types[0];
            } else if (isMap && types != null && types.length > 1) {
                if (types[1] instanceof Class) {
                    this.type = (Class) types[1];
                }
            }
        }
    }

}
